pub mod ffi;
pub mod core;
pub mod get_funcs;
pub mod set_funcs;

#[cfg(test)]
mod tests {
    // use libc::{c_void, size_t, c_int, c_char, ptrdiff_t, c_double};
    use libc::{c_void, c_int};
    
    use crate::core::LuaEngine;

    use super::*;
    use ffi::*;
    use std::ffi::{CStr, CString};

    #[derive(Default, Debug)]
    struct Point2D {
        x: f32,
        y: f32
    }
    impl Point2D {
        pub fn get(&self, name: &String) -> f32 {
            match name.as_str() {
                "x" => self.x,
                "y" => self.y,
                _ => unreachable!()
            }
        }
        pub fn set(&mut self, name: &String, value: f32) {
            match name.as_str() {
                "x" => self.x = value,
                "y" => self.y = value,
                _ => unreachable!()
            }
        }
        pub fn _getter(self_instance: *const Self, getter_str: &String) -> f32 {
            unsafe {
                let self_ref = &*self_instance;
                match getter_str.as_str() {
                    "x" => self_ref.x,
                    "y" => self_ref.y,
                    _ => unreachable!()
                }
            }
        }
    }

    unsafe extern "C" fn get_distributor(state: *mut lua_State) -> c_int {
        // println!("[Rust] In get_distributor");
        // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | INDEX_STR |                            PARAMS FROM LUA

        let index_str = CStr::from_ptr(lua_tolstring(state, -1, std::ptr::null_mut())).to_owned();
        lua_settop(state, -2);
        // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE |                                        GET ACCESS INDEX_STR

        let object_address;
        match lua_type(state, 1) {
            2 => {
                object_address = lua_touserdata(state, 1) as usize as i64;
            },
            5 => {
                let c_string = CString::new("__ObjectAddress").expect("");

                lua_pushstring(state, c_string.as_ptr());
                // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | __ObjectAddress |              SET ACCESS TABLE INDEX_STR
                lua_gettable(state, -2);
                // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | TABLE.__ObjectAddress |        FIND TABLE.__ObjectAddress
                object_address = lua_tointegerx(state, -1, std::ptr::null_mut());
                // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | TABLE.__ObjectAddress |        GET VALUE TABLE.__ObjectAddress
                lua_settop(state, -2);
                // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE |                                POP VALUE TABLE.__ObjectAddress
            },
            _ => unreachable!()
        }
        // Able to Find a Address
        if object_address != 0 {
            // println!("[Rust] Get Object Address from Lua: {:?}", object_address as *mut c_void);

            let instance = object_address as *const Point2D;
            let instance_ref = &*instance;
            let get_value = instance_ref.get(&index_str.to_str().expect("!!!").to_string());
            println!("[Rust] Get Point2D.{:?}, {}", &index_str, get_value);

            lua_settop(state, -2);
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····|                                            CLEAR
            lua_pushnumber(state, get_value.into());
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| get_value |                                PUSH RESULT

            return 1
        }
        
        0
    }

    unsafe extern "C" fn set_distributor(state: *mut lua_State) -> c_int {
        // println!("[Rust] In set_distributor");

        // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | INDEX_STR | VALUE |                           PARAMS FROM LUA

        let index_str = CStr::from_ptr(lua_tolstring(state, 2, std::ptr::null_mut())).to_owned();
        
        let object_address;
        match lua_type(state, 1) {
            2 => {
                object_address = lua_touserdata(state, 1) as usize as i64;
            },
            5 => {
                let c_string = CString::new("__ObjectAddress").expect("");

                lua_pushstring(state, c_string.as_ptr());
                // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | INDEX_STR | VALUE | __ObjectAddress |              SET ACCESS TABLE INDEX_STR
                lua_gettable(state, 1);
                // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | INDEX_STR | VALUE | TABLE.__ObjectAddress |        FIND TABLE.__ObjectAddress
                object_address = lua_tointegerx(state, -1, std::ptr::null_mut());
                // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | INDEX_STR | VALUE | TABLE.__ObjectAddress |        GET VALUE TABLE.__ObjectAddress
            
                lua_settop(state, -2);
                // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | INDEX_STR | VALUE |                                POP VALUE TABLE.__ObjectAddress
            },
            _ => unreachable!()
        }

        if object_address != 0 {
            // println!("[Rust] Get Object Address from Lua: {:?}", object_address as *mut c_void);    
            
            let value = lua_tonumberx(state, 3, std::ptr::null_mut());
            // println!("Stack Num: {}", lua_gettop(state));             //  |·····| TABLE | INDEX_STR | VALUE |                                POP VALUE TABLE.__ObjectAddress
            
            let instance = object_address as *mut Point2D;
            let instance_ref = &mut *instance;
            instance_ref.set(&index_str.to_str().expect("!!!").to_string(), value as f32);
            println!("[Rust] Set Point2D.{:?}, {}", &index_str, value);
        }

        // Deal Lua Logic
        // lua_rawset(state, 1);
        // lua_settop(state, -2);
        lua_settop(state, 0);

        0
    }

    unsafe fn lua_dump_stack(state: *mut lua_State) {
        let stack_top = lua_gettop(state);

        for i in 1..=stack_top {
            let element_type = lua_type(state, i);
            let type_str;
            let output;
            match element_type {
                0 => {
                    output = CString::new("nil").unwrap();
                    type_str = String::from("nil            ");
                },
                1 => {
                    output = CString::new(lua_toboolean(state, i).to_string()).unwrap();
                    type_str = String::from("boolean        ");
                },
                2 => {
                    output = CString::new((lua_touserdata(state, i) as usize).to_string()).unwrap();
                    type_str = String::from("lightuserdata  ");
                },
                3 => {
                    let is_integer = lua_isinteger(state, i) != 0;
                    if is_integer {
                        output = CString::new(lua_tointegerx(state, i, std::ptr::null_mut()).to_string() + "_" + &is_integer.to_string()).unwrap();
                    } else {
                        output = CString::new(lua_tonumberx(state, i, std::ptr::null_mut()).to_string() + "_" + &is_integer.to_string()).unwrap();
                    }
                    type_str = String::from("number         ");
                },
                4 => {
                    output = CStr::from_ptr(lua_tolstring(state, i, std::ptr::null_mut())).to_owned();
                    type_str = String::from("string         ");
                },
                5 => {
                    output = CString::new("todo").unwrap();
                    type_str = String::from("table          ");
                },
                6 => {
                    output = CString::new("todo").unwrap();
                    type_str = String::from("function       ");
                },
                7 => {
                    output = CString::new((lua_touserdata(state, i) as usize).to_string()).unwrap();
                    type_str = String::from("userdata       ");
                },
                8 => {
                    output = CString::new("todo").unwrap();
                    type_str = String::from("thread         ");
                },
                _ => {
                    output = CString::new("unknown").unwrap();
                    type_str = String::from("unknown        ");
                }
            }
            println!("Stack Index {}[{:?}]: {:?}", i, type_str, output);
        }
        println!("");
    }

    fn raw_lua_test(script: &String) {
        unsafe {
            let state = luaL_newstate();
            // luaopen_base(state);
            // luaopen_table(state);
            // luaopen_package(state);
            // luaopen_io(state);
            // luaopen_string(state);

            luaL_openlibs(state);

            // println!("Stack Num: {}", lua_gettop(state));
            lua_dump_stack(state);

            // -------------------- Add Int Value
            lua_pushinteger(state, 64);
            // println!("Stack Num: {}", lua_gettop(state));
            let c_string = CString::new("MyInt").expect("");

            lua_setglobal(state, c_string.as_ptr());
            // println!("Stack Num: {}", lua_gettop(state));

            // -------------------- Add Table Value
            let mut object = Point2D::default();
            object.x = 10.0;
            object.y = 20.0;

            let object_address = &object as *const Point2D as usize as i64;
            println!("rust object address: {:?}", std::ptr::addr_of!(object));

            lua_createtable(state, 0, 0);   
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| table |                           PUSH A TABLE
            lua_pushinteger(state, object_address);
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| table | object_address |          PUSH A INTEGER
            let c_string = CString::new("__ObjectAddress").unwrap();
            lua_setfield(state, -2, c_string.as_ptr());
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| table |                           SET TABLE.__ObjectAddress = INTEGER; POP INTEGER

            let c_string = CString::new("MyMetatable").unwrap();
            luaL_newmetatable(state, c_string.as_ptr());
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| table | meta_table |              PUSH A METATBABLE
            lua_pushcclosure(state, Some(get_distributor), 0);
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| table | meta_table | function |   PUSH A METATBABLE
            let c_string = CString::new("__index").unwrap();
            lua_setfield(state, -2, c_string.as_ptr());
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| table | meta_table |              SET METATBLE.__index = FUNCTION; POP FUNCTION
            lua_settop(state, -2);
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| table |                           POP METATABLE

            let c_string = CString::new("MyMetatable").unwrap();
            luaL_setmetatable(state, c_string.as_ptr());
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| table |                           SET TABLE.METATABLE = MyMetatable
            let c_string = CString::new("MyTable").unwrap();
            lua_setglobal(state, c_string.as_ptr());
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····|                                   SET GLOBAL.MyTable = TABLE; POP TABLE

            // -------------------- Add LightUserdata
            lua_pushlightuserdata(state, &mut object as *mut Point2D as *mut c_void);
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| light_userdata |                  CREATE LIGHT USERDATA
            let c_string = CString::new("MyMetatable").unwrap();
            luaL_setmetatable(state, c_string.as_ptr());
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····| light_userdata |                  SET LIGHTUSERDATA.METATABLE = MyMetatable
            let c_string = CString::new("MyLightUserdata").unwrap();
            lua_setglobal(state, c_string.as_ptr());
            // println!("Stack Num: {}", lua_gettop(state));         //  |·····|                                   SET GLOBAL.MyLightUserdata = LIGHTUSERDATA; POP TABLE

            let c_string = CString::new(script.clone()).unwrap();
            luaL_dostring(state, c_string.as_ptr());
        }
    }

    fn pack_lua_test(script: &String) {
        let lua_engine = LuaEngine::new();

        lua_engine.dump_stack();
        lua_engine.set_global_integer("MyInt", 64);

        // Arrange Object Data
        let mut object = Point2D::default();
        object.x = 10.0;
        object.y = 20.0;
        let object_address = &object as *const Point2D as usize as i64;

        println!("[Rust] {:?}", object);

        // Create Metatable
        lua_engine.create_metatable("MyMetatable");
        lua_engine.set_table_cfunction("__index", &Some(get_distributor), 0);
        lua_engine.set_table_cfunction("__newindex", &Some(set_distributor), 0);
        lua_engine.pop_stack(1);

        // Table Strategy
        lua_engine.create_table();
        lua_engine.set_table_integer("__ObjectAddress", &object_address, 0);
        lua_engine.set_metatable("MyMetatable", 0);
        lua_engine.set_global_value("MyTable", 0);

        // LightUserdata Strategy
        lua_engine.push_lightuserdata(&mut object as *mut Point2D as *mut c_void);
        lua_engine.set_metatable("MyMetatable", 0);
        lua_engine.set_global_value("MyLightUserdata", 0);

        lua_engine.run_script(script);
        
        println!("[Rust] {:?}", object);
    }

    #[test]
    fn test_lua() {
        let script = String::from(
            r#"
    print("[During Lua] START")
    print(MyInt)
    print(MyTable)
    print(MyLightUserdata)

    print("[    ] Point2D.x:", MyTable.x);
    print("[    ] Point2D.y:", MyTable.y);

    MyTable.x = MyTable.x * 20.0;
    MyLightUserdata.y = MyLightUserdata.y / 10.0;

    print("[    ] Point2D.x:", MyLightUserdata.x);
    print("[    ] Point2D.y:", MyLightUserdata.y);

    print("[    ] lua light userdata: ", MyLightUserdata)
    print("[    ] lua object address: ", string.format("0x%x", MyTable.__ObjectAddress))

    print("[During Lua] END")
            "#
        );

        // raw_lua_test(&script);
        pack_lua_test(&script);

    }
}